package com.flex.util.proxy {

   import com.flex.util.MixedUtil;
   
   import flash.events.TimerEvent;
   import flash.utils.Timer;
   
   import mx.messaging.events.ChannelFaultEvent;
   import mx.rpc.AbstractOperation;
   import mx.rpc.AsyncToken;
   import mx.rpc.Fault;
   import mx.rpc.IResponder;
   import mx.rpc.events.FaultEvent;
   import mx.rpc.events.ResultEvent;

   internal class OperationInvoker implements IResponder {

      private static const DEFAULT_RETRY_COUNT:int = 10;
      private static const DEFAULT_RETRY_PERIOD:Number = 500; //In ms.
      private static const NETCONNECTION_CALL_FAILED:String = "NetConnection.Call.Failed";
      private static const HTTP_FAILED:String = "HTTP: Failed";

      public var retryCount:int = DEFAULT_RETRY_COUNT;

      public var retryPeriod:Number = DEFAULT_RETRY_PERIOD;

      private var _timer:Timer;
      private var _currentRetryCount:int = 0;
      private var _retryTokens:Array;

      public function invoke(
         operation:AbstractOperation,
         arguments:Array,
         callback:Function,
         callContext:Object):void {
         operation.arguments = arguments;
         var token:AsyncToken = operation.send();
         token.addResponder(this);
         token.callback = callback;
         token.callContext = callContext;
         token.operation = operation;
      }

      public function result(data:Object):void {
         var event:ResultEvent = data as ResultEvent;
         if (event == null) {
            return;
         }
         // Since this is a successful response replay all requests
         replayFailedOperations();
         var token:AsyncToken = event.token;
         resultResponseForRequest(event, token);
      }

      public function fault(info:Object):void {
         var event:FaultEvent = info as FaultEvent;
         if (event == null) {
            return;
         }
         var token:AsyncToken = event.token;
         if (retryOnFault(event, token)) {
            return;
         }
         faultResponseForRequest(event, token);
      }

      private function resultResponseForRequest(event:ResultEvent, token:AsyncToken):void {
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         if (callback != null) {
            callback(event, callContext);
         }
      }

      private function faultResponseForRequest(event:FaultEvent, token:AsyncToken):void {
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         if (callback != null) {
            callback(event, callContext);
         }
      }

      private function replayOperationOnTimer(token:AsyncToken):void {
         if (_timer == null) {
            _timer = new Timer(retryPeriod, 1);
            _timer.addEventListener(TimerEvent.TIMER, onTimerEvent);
         }
         if (_retryTokens == null) {
            _retryTokens = [];
         }
         // As an extra precaution remove responder from this token.
         MixedUtil.removeItemFromArray(this, token.responders);
         // Adding to back of the retry token queue so that it can be used to replay
         _retryTokens.push(token);
         // If the timer is already running from a previous replay attempt then just queue
         // up the token for replay
         if (!_timer.running) {
            _timer.reset();
            _timer.start();
         }
      }

      /**
       * Picks the first token from the tokens list and replays the contained operation.
       */
      private function onTimerEvent(event:TimerEvent):void {
         // stop the timer to prevent any more events until the next fault is detected.
         _timer.stop();
         // Nothing to retry
         if (_retryTokens.length < 1) {
            return;
         }
         // Pick up the first token in the retry list. It really does not matter which
         // request is replayed so no single replay token is maintained.
         var token:AsyncToken = _retryTokens.shift();
         // increment retry counter
         ++_currentRetryCount;
         replayOperation(token);
      }

      /**
       * Replays all the failed operations.
       */
      private function replayFailedOperations():void {
         if (_currentRetryCount > 0) {
         }
         _currentRetryCount = 0;
         clearTimer();
         if (MixedUtil.isNullOrEmpty(_retryTokens)) {
            return;
         }
         // Replay all request in a tight loop.
         for each (var token:AsyncToken in _retryTokens) {
            replayOperation(token);
         }
         _retryTokens = null;
      }

      /**
       * Replays the operation contained in the supplied token.
       */
      private function replayOperation(token:AsyncToken):void {
         var operation:AbstractOperation = token.operation;
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         invoke(operation, operation.arguments as Array, callback, callContext);
      }

      private function faultResponseToFailedOperations(event:FaultEvent):void {
         clearTimer();
         if (MixedUtil.isNullOrEmpty(_retryTokens)) {
            return;
         }
         // Replay all request in a tight loop.
         for each (var token:AsyncToken in _retryTokens) {
            faultResponseForRequest(event, token);
         }
         _retryTokens = null;
      }

      private function retryOnFault(event:FaultEvent, token:AsyncToken):Boolean {
         if (event == null || event.fault == null) {
            return false;
         }
         var retriableFailure:Boolean = isPotentialNetworkFailure(event.fault);
         if (retriableFailure && event.fault != null) {
         } else {
            replayFailedOperations();
            return false;
         }
         if (_currentRetryCount < this.retryCount) {
            replayOperationOnTimer(token);
            return true;
         }
         faultResponseToFailedOperations(event);
         return false;
      }

      private function isPotentialNetworkFailure(fault:Fault):Boolean {
         var channelFaultEv:ChannelFaultEvent = fault.rootCause as ChannelFaultEvent;
         if (channelFaultEv == null || channelFaultEv.rootCause == null) {
            return false;
         }
         return channelFaultEv.rootCause.code == NETCONNECTION_CALL_FAILED &&
            channelFaultEv.rootCause.description == HTTP_FAILED;
      }

      private function clearTimer():void {
         if (_timer == null) {
            return;
         }
         _timer.stop();
         _timer.removeEventListener(TimerEvent.TIMER, onTimerEvent);
         _timer = null;
      }
   }
}